jqを活用してAPIレスポンスから欲しい情報を抽出する【上級編】
よく訓練されたアップル信者、都元です。引き続きjq
のお話。本日は上級編です。
まずおさらいの意味も込めて下記は、ざっと読んでおいて頂けると。
前回の練習問題の答え
さて、まず前回の練習問題の答えをさらっと。1つ目の解答例は下記の通りです。「どっと・ふー・むきむき」と読んでください。
$ cat example.json | jq '.foo[][]'
2つ目の解答例はこんな感じでしょうか。
$ cat example.json | jq '[.foo[][] | {(.key): .value}] | add'
jqにおける関数
さて。
ある値をから別の値を作る操作のことを関数(function)と呼びます。例えば今まで見てきたjqのクエリはそれぞれ全てが(あるJSONから別のJSONを作る)関数です。関数を使って入力値から出力値を計算することを、関数の適用(apply)と呼びます。
.
は、入力値をそのまま出力する関数(恒等関数)です。.coord
は、入力値の一部分だけを抽出したものを出力する関数です。.coord.lon
は、.coord
を適用した後に.lon
を適用する関数(合成関数)です。つまり|
というのは関数を合成する演算子だったのです。
さてここで、例えば「値に10を足す」という関数があったとします。2
に対してこの関数を適用すると12
ですね? jqではこの関数を. + 10
と書きます。コマンドはこんな感じです。
$ echo 10 | jq ". + 10" 20
jqのクエリというのは、最小の構成単位が関数で、それが複雑に合成されて、その結果として目的の関数ができているんだなぁ、ということを感じてください。
select関数
配列中の多くの要素の中から、特定の条件を満たすものだけを取り出す操作を絞り込み(filter)と呼びます。例えば、 [3,-1,6,1000,19]
という配列の中から、0以上100未満という条件で絞り込みをすると、結果は [3,6,19]
となります。このセクションではこの絞り込み操作をゴールとしたいと思いますが、それを実現する要素を一つ一つ確認しましょう。
この処理をする前に、この「0以上100未満という条件」を判定する関数を考えましょう。. >= 0 and . < 100
ですね。and
は初出ですがまぁわかりますね? 他にも or
や not
もあります。
さて、この関数を値に適用してどのような結果となるか見てみましょう。
$ echo -10 | jq '. >= 0 and . < 100' false $ echo 10 | jq '. >= 0 and . < 100' true $ echo 100 | jq '. >= 0 and . < 100' false $ echo 99 | jq '. >= 0 and . < 100' true $ echo 1000 | jq '. >= 0 and . < 100' false
うん、期待通り動いていそうですね。さてここで、値に対してとある関数を適用した時、結果が真(true)となったら元の値をそのまま出力、結果が偽(false)となったら元の値は捨てて空(empty)の出力を行う、という関数select
の登場です。jqの記述としては、select(. >= 0 and . < 100)
です。
$ echo -10 | jq 'select(. >= 0 and . < 100)' $ echo 10 | jq 'select(. >= 0 and . < 100)' 10 $ echo 100 | jq 'select(. >= 0 and . < 100)' $ echo 99 | jq 'select(. >= 0 and . < 100)' 99 $ echo 1000 | jq 'select(. >= 0 and . < 100)'
分かるでしょうか。1つ前の例で結果がtrue
になっていたものだけ入力値がそのまま出力され、false
だったものは空出力になっています。今ひとつ理解が追いつかない方は、もう少し冗長で直感的な、下記と見比べてみてください。if . >= 0 and . < 100 then . else empty end
は select(. >= 0 and . < 100)
と等価な関数です。
$ echo 99 | jq 'if . >= 0 and . < 100 then . else empty end' 99 $ echo 1000 | jq 'if . >= 0 and . < 100 then . else empty end'
さてここで絞り込みに戻ります。ここまでの知識がしっかり身についていれば、あとは絞り込みができます。まず[3,-1,6,1000,19]
という入力に対して皮むき(配列の展開)して、select関数を適用した後、その結果をArray construction(配列の構築)で配列にすればいいですね。って下記の実例を見てしまうのが早いと思います。
$ echo '[3,-1,6,1000,19]' | jq -c '[.[] | select(. >= 0 and . < 100)]' [3,6,19]
空出力の値は、Array construction時の要素からは除外されるので、結果的に絞り込みができました。
map関数
初級編でも「上級編な話」として一瞬触れたmap
関数について。mapとは日本語で言うと写像です。
mapは、配列に対して、その全ての要素にそれぞれ関数を適用し、結果の配列を得る、という関数です。
つまり[3,6,19]
という配列に対するこの関数の写像は? [13,16,29]
というわけです。
$ echo '[3,6,19]' | jq -c 'map(. + 10)' [13,16,29]
おや、そういえば上記の絞り込みの処理は「配列に対して、その全ての要素にそれぞれselect関数を適用し、結果の配列を得る」というものでしたね。これもmap処理じゃないでしょうか? その通りです。前述のフィルター処理はこのように書けます。
$ echo '[3,-1,6,1000,19]' | jq -c 'map(select(. >= 0 and . < 100))' [3,6,19]
さらにmap関数を|
で合成して…
$ echo '[3,-1,6,1000,19]' | jq -c 'map(select(. >= 0 and . < 100)) | map(. + 10)' [13,16,29]
というふうに、最初の入力値[3,-1,6,1000,19]
を次々に変換していきます。
map(function)
という記述は[.[] | function]
の別の書き方と考えて良いでしょう。
reduce関数
はい、filter・mapと来たら次はもちろんreduceですね。例えば、配列の全要素の総和(Σ)や相乗(Π)を求めたい、という話です。具体的には、[13,16,29]
の総和は13+16+29で58
です。相乗は13×16×29で6032
です。
えーと、これ説明が難しいので、まず答えを示します。ちょっと複雑な関数ですね。
$ echo '[13,16,29]' | jq 'reduce .[] as $item (0; . + $item)' 58 $ echo '[13,16,29]' | jq 'reduce .[] as $item (1; . * $item)' 6032
プログラミングの経験のある方がこれを見れば、.[]
によって展開された値を順次$item
に代入しながらループを回すんだな、という理解ができると思います。そして最後の括弧の中身ですが、;
の手前にある値(0や1)のことを単位元(identity element)と呼びます。;
の後ろにある関数を累積関数(accumulatorやaccumulation function)と呼びます。
単位元とは、行いたい計算の片方の引数に与えたとき、もう一方の引数がそのまま返るような値です。総和では、行いたい計算は「和」つまり足し算です。とある数字aに足した時、結果がそのままaとなるような値は? 0
ですね。なので、和の単位元は0
です。相乗では、行いたい計算は「積」つまり掛け算です。とある数字aに掛けた時、結果がそのままaとなるような値は? 1
ですね。なので積の単位元は1
です。
累積関数というのは、配列の各要素を使って作る関数で、「前の累積関数適用結果」を入力値として関数適用を行います。ただし、最初は「前の累積関数適用結果」が存在しません。その時は単位元を利用します。。。わかりにくいですね。。。具体的な動きを下記にまとめました。下記は総積の例です。
- まず、初期値を単位元(1)とします。
- この値に対して、
. * $item
を適用します。最初の適用時には$item
は13
なので、具体的には. * 13
を適用することになり、その結果は13
です。 - 次に、この
13
に対してさらに、. * $item
を適用します。この時$item
は16
なので、結果は208
です。 - 最後にこの
208
に対して、. * 29
を適用することになり、最終結果は6032
となります。
難しいですかね…。にもかかわらず練習問題。
- 「文字列と文字列をつなぐ関数」があったとします。つまり"foo"+"bar"から"foobar"を作る
+
という関数です。この関数の単位元はなんでしょう? ["foo", "bar", "baz"]
から"foobarbaz"
を得る関数を、reduce
関数を用いて実装してください。つまり下記です。
$ echo '["foo", "bar", "baz"]' | jq 'reduce 【ここを埋めよ】' "foobarbaz"
最後に、ここまでのfilter・map・reduceの流れを一気に書くと…。
$ echo '[3,-1,6,1000,19]' | jq -c ' map(select(. >= 0 and . < 100)) | map(. + 10) | reduce .[] as $item (1; . * $item)' 6032
こんな感じです。いわゆる MapReduce と呼ばれるプログラミングモデルで関数が記述できました!
まとめ
今回は上級編として、jqにおける関数の概念を再確認しました。その上でfilter・map・reduceという、MapReduceプログラミングモデルの構成要素をそれぞれ説明しました。
実は上級編では、AWS CLIの出力を複雑に処理していく実例を示そうと思っていたのですが、これにはfilterとmapをしっかり身につけなければ難しい、ということになり、ついでにreduceまで説明してしまいました。
というわけで、次回こそ最終回【エキスパート実践編】としてお送りする予定です。お楽しみに。